﻿using LunarSF.SHomeWorkshop.LunarMarkdownEditor.Utils.NPOIHelp;
using MahApps.Metro.Controls;
using Microsoft.WindowsAPICodePack.Dialogs;
using OpenCvSharp;
using OpenCvSharp.Extensions;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Xml;

namespace LunarSF.SHomeWorkshop.LunarMarkdownEditor
{
    /// <summary>
    /// AnswerSheetReader.xaml 的交互逻辑
    /// </summary>
    public partial class AnswerSheetReader : MetroWindow
    {
        public AnswerSheetReader()
        {
            InitializeComponent();

            // 高亮显示识别结果文本框
            Stream s = typeof(MainWindow).Assembly.GetManifestResourceStream("LunarSF.SHomeWorkshop.LunarMarkdownEditor.AnswerSheetReader.SyntaxHightlightRulesets.xshd");
            if (s != null)
            {
                using (XmlReader reader = new XmlTextReader(s))
                {
                    var findAndReplaceHighlight = ICSharpCode.AvalonEdit.Highlighting.Xshd.HighlightingLoader.Load(reader, ICSharpCode.AvalonEdit.Highlighting.HighlightingManager.Instance);

                    ICSharpCode.AvalonEdit.Highlighting.HighlightingManager.Instance.RegisterHighlighting("Answer Sheet Reader Highlighting", new string[] { ".fr" }, findAndReplaceHighlight);
                    tbxResult.SyntaxHighlighting = ICSharpCode.AvalonEdit.Highlighting.HighlightingManager.Instance.GetDefinitionByExtension(".fr");
                }
            }

            var minRateOfNumbers = App.WorkspaceConfigManager.Get("MinRateOfAnswerSheetNumbers");
            if (minRateOfNumbers != null && double.TryParse(minRateOfNumbers.ToString(), out var minRateOfN))
            {
                sliderOfMinMatchRateForNumbers.Value = minRateOfN;
            }

            var minRateOfQuestions = App.WorkspaceConfigManager.Get("MinRateOfAnswerSheetQuestions");
            if (minRateOfQuestions != null && double.TryParse(minRateOfQuestions.ToString(), out var minRateOfQ))
            {
                sliderOfMinMatchRateForQuestions.Value = minRateOfQ;
            }

            #region 初始化识别结果框样式

            tbxResult.SnapsToDevicePixels = true;//避免折叠线模糊
            tbxResult.Background = System.Windows.Media.Brushes.WhiteSmoke;

            tbxResult.WordWrap = false;
            tbxResult.ShowLineNumbers = false;
            tbxResult.Options.ShowColumnRuler = true;
            tbxResult.Options.ShowSpaces = true;
            //AvalonEdit本来使用间隔号代替空格，但间隔号是全角字符，要占据两个半角的宽度；
            //我把它改为使用加重号，这样就只需要占据一个半角宽度了。
            tbxResult.Options.ShowTabs = true;
            tbxResult.Options.AutoAlignTabs = true;//将Tab显示为“--+”，并禁止Tab自动伸缩尺寸。

            TextOptions.SetTextFormattingMode(tbxResult, TextFormattingMode.Display);

            tbxResult.Options.ShowBoxForControlCharacters = true;
            tbxResult.Options.ShowEndOfLine = true;
            tbxResult.Options.AllowScrollBelowDocument = true;
            tbxResult.Options.HighlightCurrentLine = false;
            tbxResult.Options.ShowColumnRuler = true;

            tbxResult.TextArea.SelectionBorder = new System.Windows.Media.Pen(System.Windows.Media.Brushes.Transparent, 1);
            tbxResult.TextArea.SelectionBrush = new SolidColorBrush(System.Windows.Media.Color.FromArgb(255, 145, 83, 0));  // Color.FromArgb(255, 240, 106, 0) 是那两条行高亮线色
            tbxResult.TextArea.SelectionCornerRadius = 0;

            tbxResult.TextArea.IndentationStrategy = new ICSharpCode.AvalonEdit.Indentation.DefaultIndentationStrategy();

            tbxResult.TextArea.Caret.PositionChanged += Caret_PositionChanged;

            #endregion

            #region 学生花名册

            tbxStudentNames.SnapsToDevicePixels = true;//避免折叠线模糊
            tbxStudentNames.Background = System.Windows.Media.Brushes.WhiteSmoke;

            tbxStudentNames.WordWrap = false;
            tbxStudentNames.ShowLineNumbers = false;
            tbxStudentNames.Options.ShowColumnRuler = true;
            tbxStudentNames.Options.ShowSpaces = true;
            //AvalonEdit本来使用间隔号代替空格，但间隔号是全角字符，要占据两个半角的宽度；
            //我把它改为使用加重号，这样就只需要占据一个半角宽度了。
            tbxStudentNames.Options.ShowTabs = true;
            tbxStudentNames.Options.AutoAlignTabs = true;//将Tab显示为“--+”，并禁止Tab自动伸缩尺寸。
            tbxStudentNames.Options.ConvertTabsToSpaces = false;

            TextOptions.SetTextFormattingMode(tbxStudentNames, TextFormattingMode.Display);

            tbxStudentNames.Options.ShowBoxForControlCharacters = true;
            tbxStudentNames.Options.ShowEndOfLine = true;
            tbxStudentNames.Options.AllowScrollBelowDocument = true;
            tbxStudentNames.Options.HighlightCurrentLine = false;
            tbxStudentNames.Options.ShowColumnRuler = true;

            tbxStudentNames.TextArea.SelectionBorder = new System.Windows.Media.Pen(System.Windows.Media.Brushes.Transparent, 1);
            tbxStudentNames.TextArea.SelectionBrush = new SolidColorBrush(System.Windows.Media.Color.FromArgb(255, 145, 83, 0));  // Color.FromArgb(255, 240, 106, 0) 是那两条行高亮线色
            tbxStudentNames.TextArea.SelectionCornerRadius = 0;

            tbxStudentNames.TextArea.IndentationStrategy = new ICSharpCode.AvalonEdit.Indentation.DefaultIndentationStrategy();
            tbxStudentNames.Text = "学生考号\t学生姓名\r\n";

            tbxStudentNames.Select(tbxStudentNames.Document.GetLineByNumber(2).Offset, 0);
            #endregion

            LoadAnswerSheetDefinitionFiles();
        }

        private void Caret_PositionChanged(object sender, EventArgs e)
        {
            var curLine = tbxResult.Document.GetLineByOffset(tbxResult.SelectionStart);
            if (curLine != null && curLine.LineNumber >= 1 && curLine.LineNumber <= lbxFiles.Items.Count)
            {
                try
                {
                    lbxFiles.SelectedIndex = curLine.LineNumber - 1;
                    lbxFiles.ScrollIntoView(lbxFiles.SelectedItem);
                }
                catch (Exception ex)
                {
                    LMessageBox.ShowWarning(ex.Message);
                }
            }
        }

        private void LoadAnswerSheetDefinitionFiles()
        {
            if (cmbPathOfAnswerSheetDefinitionFile.Items == null) return;

            cmbPathOfAnswerSheetDefinitionFile.Items.Clear();
            var files = GetDefinitionFiles(Globals.PathOfWorkspaceAssetsFolder);
            if (files != null && files.Count > 0)
            {
                foreach (var file in files)
                {
                    cmbPathOfAnswerSheetDefinitionFile.Items.Add(new AnswerSheetDefinitionFileItem(file));
                }
            }

            if (cmbPathOfAnswerSheetDefinitionFile.Items.Count == 1)
            {
                cmbPathOfAnswerSheetDefinitionFile.SelectedIndex = 0;
            }
        }

        public Bitmap MatToBitmap(Mat mat)
        {
            if (mat == null) return new Bitmap(Globals.InstalledPath + "app.png");
            return BitmapConverter.ToBitmap(mat);  // OpenCV的BGR格式自动转为System.Drawing的RGB
        }

        public BitmapImage MatToBitmapImage(Mat mat)
        {
            using (var bitmap = MatToBitmap(mat))
            {
                using (var stream = new MemoryStream())
                {
                    bitmap.Save(stream, System.Drawing.Imaging.ImageFormat.Png);  // 保存为PNG格式
                    stream.Position = 0;

                    var bitmapImage = new BitmapImage();
                    bitmapImage.BeginInit();
                    bitmapImage.CacheOption = BitmapCacheOption.OnLoad;  // 立即加载避免流释放问题
                    bitmapImage.StreamSource = stream;
                    bitmapImage.EndInit();
                    bitmapImage.Freeze();  // 解除UI线程束缚
                    return bitmapImage;
                }
            }
        }

        internal void BtnReadSelectedItem_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                if (lbxFiles.SelectedItem == null || lbxFiles.SelectedItem is FileListBoxItem == false)
                {
                    LMessageBox.ShowWarning("请先载入要识别的答题卡文件（或目录）");
                    return;
                }

                var fi = lbxFiles.SelectedItem as FileListBoxItem;

                if (File.Exists(fi.FilePath) == false)
                {
                    LMessageBox.ShowWarning("选定项指向的答题卡图片文件不存在！");
                    return;
                }

                if (cmbPathOfAnswerSheetDefinitionFile.SelectedIndex < 0)
                {
                    LMessageBox.ShowWarning("请先选择要使用的答题卡定义文件！");
                    return;
                }

                var ad = new 答题卡定义(System.IO.File.ReadAllText((cmbPathOfAnswerSheetDefinitionFile.SelectedItem as AnswerSheetDefinitionFileItem).FilePath));
                if (ad.边框宽度比 == null || ad.边框宽度比.HasValue == false || ad.边框高度比 == null || ad.边框高度比.HasValue == false)
                {
                    LMessageBox.ShowWarning("答题卡定义有问题，未找到“边框宽度比”或“边框高度比”的值，无法进行切边操作。请使用最新版 LME 导出答题卡定义文本文件。");
                    return;
                }

                if (ad.编号条目列表 == null || ad.编号条目列表.Count < 1)
                {
                    LMessageBox.ShowWarning("答题卡定义有问题，未找到“编号”填涂定义，无法判断是谁的成绩。");
                    return;
                }

                if (ad.试题条目列表 == null || ad.试题条目列表.Count < 1)
                {
                    LMessageBox.ShowWarning("答题卡定义有问题是，未找到试题填涂定义，无法读取。");
                    return;
                }

                var valTxt1 = sliderOfMinMatchRateForQuestions.Value.ToString();
                tbMinMatchRateForQuestions.Text = valTxt1 + "%";
                App.WorkspaceConfigManager.Set("MinRateOfAnswerSheetQuestions", valTxt1);

                var valTxt2 = sliderOfMinMatchRateForNumbers.Value.ToString();
                tbMinMatchRateForNumbers.Text = valTxt2 + "%";
                App.WorkspaceConfigManager.Set("MinRateOfAnswerSheetNumbers", valTxt2);

                var answer = tbxSetAnswer.Text.ToUpper();
                if (answer != null) answer = answer.Replace(" ", "").Replace("　", "").Replace("\t", "");

                if (string.IsNullOrEmpty(answer) == false)
                {
                    // 设置了答案字符串
                    if (answer.Contains(",") || answer.Contains("，") || answer.Contains(";") || answer.Contains("；"))
                    {
                        // 不定项选择题或多项选择题
                        // 答案以分号、逗号分隔（如无分号逗号则视为单选题，如无答案则以答题卡定义文件中的答案为准）
                        // 分号用以分隔试题，逗号用以分开题号与答案。

                        var quesSpans = answer.Split(new char[] { ';', '；' }, StringSplitOptions.RemoveEmptyEntries);

                        for (int i = 0; i < quesSpans.Length; i++)
                        {
                            var qSpan = quesSpans[i];
                            if (qSpan.Contains(",") || qSpan.Contains("，"))
                            {
                                // 指定题号
                                var optionSpans = qSpan.Split(new char[] { ',', '，' }, StringSplitOptions.RemoveEmptyEntries);
                                if (optionSpans.Length >= 2)
                                {
                                    var ques = ad.取试题条目(optionSpans[0]);
                                    if (ques == null)
                                    {
                                        LMessageBox.ShowWarning($"第{i + 1}题给定的题号不存在，无法继续。");
                                        return;
                                    }

                                    ques.置答案文本(optionSpans[1].ToUpper());
                                }
                                else
                                {
                                    // 实际上未提供题号，出错。
                                    LMessageBox.ShowWarning($"第{i + 1}题写了逗号，但未给出题号或答案，无法继续。");
                                    return;
                                }
                            }
                            else
                            {
                                // 自动题号
                                if (i < ad.试题条目列表.Count)
                                {
                                    ad.试题条目列表[i].置答案文本(qSpan.ToUpper());
                                }
                            }
                        }
                    }
                    else
                    {
                        // 单选题
                        if (ad.试题条目列表 != null && ad.试题条目列表.Count > 0)
                        {
                            if (answer.Length > ad.试题条目列表.Count)
                            {
                                LMessageBox.ShowWarning("答案字符串过长！");
                            }

                            for (int i = 0; i < answer.Length; i++)
                            {
                                if (ad.试题条目列表[i].填涂选项集 == null || ad.试题条目列表[i].填涂选项集.Count < 0) continue;

                                foreach (var o in ad.试题条目列表[i].填涂选项集)
                                {
                                    o.置是否答案(false);
                                }

                                for (int j = 0; j < ad.试题条目列表[i].填涂选项集.Count; j++)
                                {
                                    var option = ad.试题条目列表[i].填涂选项集[j];
                                    if (option != null && option.选项文本 != null && option.选项文本.ToUpper() == answer[i].ToString().ToUpper())
                                    {
                                        option.置是否答案(true);
                                    }
                                }
                            }
                        }
                    }
                }// 未设置，以答题卡定义中的答案为准

                var 精确匹配模式 = ckxStrictMatchingMode.IsChecked.HasValue && ckxStrictMatchingMode.IsChecked.Value;
                var 是否带批改信息 = ckxWithCorrectInfo.IsChecked.HasValue && ckxWithCorrectInfo.IsChecked.Value;
                var 用冒号分隔答案 = ckxSplitBetweenQuestions.IsChecked.HasValue && ckxSplitBetweenQuestions.IsChecked.Value;
                var autoConnectOutBorder = ckxAutoConnectOutBorder.IsChecked.HasValue && ckxAutoConnectOutBorder.IsChecked.Value;

                ad.编号最小匹配比例 = sliderOfMinMatchRateForNumbers.Value;
                ad.试题最小匹配比例 = sliderOfMinMatchRateForQuestions.Value;
                ad.读卡时尝试连接外框缺口 = autoConnectOutBorder;

                var numbersList = new List<string>();
                var answersList = new List<string>();

                Mat srcMat = null;
                var readResultMat = ReadAnswerSheet.匹配答案(fi.FilePath, ad, ref numbersList, ref answersList, out int fzfgCount,
                                                             精确匹配模式, 是否带批改信息, 定位模式.四角定位, ref srcMat);
                imgPreview.Source = MatToBitmapImage(readResultMat);
                imgPreviewSource.Source = MatToBitmapImage(srcMat);

                var numSB = new StringBuilder();
                var ansSB = new StringBuilder();

                foreach (var x in numbersList)
                {
                    numSB.Append(x);
                }

                if (answersList.Count > 0)
                {
                    for (int i = 0; i < answersList.Count - 1; i++)   // 最后一个不在循环里添加
                    {
                        var y = answersList[i];
                        ansSB.Append(y + (用冒号分隔答案 ? "; " : ""));
                    }
                    ansSB.Append(answersList[answersList.Count - 1]);  // 末尾不加分号
                }

                fi.识别编号 = numSB.ToString();
                fi.识别答案 = ansSB.ToString();

                tbsResultOfSelectedItem.Text = $"学号：{numSB}\t答案：{ansSB}";

                // 判断识别状态
                // 基本规则：
                //     1. 无论是不定项、多项还是单项，都检查
                //     2. 先检查是否每个编号都只填涂了一位数字
                //     3. 再检查是否道题至少填涂了一个答案
                //     4. 未填涂任何方块者返回错误，有缺失的为存疑

                if (ad.答题卡宽高比.HasValue && readResultMat != null)
                {
                    var realRate = (double)readResultMat.Width / readResultMat.Height;

                    // 判断宽高比
                    if (realRate < ad.答题卡宽高比 - 0.4 || realRate > ad.答题卡宽高比 + 0.4)
                    {
                        fi.识别状态 = 识别状态.ER;
                        fi.错误信息 += "答题卡宽高比异常幅度大于 0.4。";
                    }
                    else if (realRate < ad.答题卡宽高比 - 0.2 || realRate > ad.答题卡宽高比 + 0.2)
                    {
                        fi.识别状态 = 识别状态.WR;
                        fi.错误信息 += "答题卡宽高比异常幅度大于 0.2。";
                    }
                    else
                    {
                        if ((ad.答题卡宽高比 > 1 && realRate < 1) || (ad.答题卡宽高比 < 1 && realRate > 1))
                        {
                            fi.识别状态 = 识别状态.WR;
                            fi.错误信息 += "答题卡宽高比存在小幅度倒错。";
                        }
                    }
                }

                if (fi.识别编号.Contains("$") || fi.识别答案.Contains("$"))
                {
                    fi.识别状态 = 识别状态.ER;
                    fi.错误信息 += "有不能识别的编号或答案。";
                }

                if (fi.识别编号.Length != ad.编号条目列表.Count)
                {
                    fi.识别状态 = 识别状态.ER;
                    fi.错误信息 += "识别的编号长度不正确。";
                }

                if (fi.是否有空值 || fi.答案数目 != ad.试题条目列表.Count)
                {
                    fi.识别状态 = 识别状态.WR;
                    fi.错误信息 += "未识别到答案或识别到的答案数目不符。";
                }

                if (是否全空列表(numbersList) && 是否全空列表(answersList))
                {
                    fi.识别状态 = 识别状态.ER;
                    fi.错误信息 += "未识别到编号或未识别到答案。";
                }

                if (是否含空元素列表(numbersList) && 是否含非空元素列表(numbersList) &&
                         是否含空元素列表(answersList) && 是否含非空元素列表(answersList))
                {
                    fi.识别状态 = 识别状态.WR;
                    fi.错误信息 += "部分编号位或答案位未能识别。";
                }

                if (numbersList.Count == ad.编号条目列表.Count && answersList.Count == ad.试题条目列表.Count &&
                         是否全非空列表(numbersList) && 是否全非空列表(answersList))
                {
                    fi.识别状态 = 识别状态.FN;
                    fi.错误信息 = "";
                }

                if (fi.识别答案.Length != ad.试题条目列表.Count)
                {
                    fi.识别状态 = 识别状态.WR;
                    fi.错误信息 += "识别到的答案数目不符。";
                }
            }
            catch (Exception ex)
            {
                LMessageBox.ShowWarning(ex.Message);
            }
        }

        public static bool 是否全空列表(List<string> list)
        {
            if (list == null || list.Count <= 0) return true;

            foreach (var li in list)
            {
                if (string.IsNullOrEmpty(li) == false) return false;
            }
            return true;
        }

        public static bool 是否全非空列表(List<string> list)
        {
            if (list == null || list.Count <= 0) return false;

            foreach (var li in list)
            {
                if (string.IsNullOrEmpty(li) == true) return false;
            }
            return true;
        }

        public static bool 是否含空元素列表(List<string> list)
        {
            if (list == null || list.Count <= 0) return true;

            foreach (var li in list)
            {
                if (string.IsNullOrEmpty(li)) return true;
            }

            return false;
        }

        public static bool 是否含非空元素列表(List<string> list)
        {
            if (list == null || list.Count <= 0) return false;

            foreach (var li in list)
            {
                if (string.IsNullOrEmpty(li) == false) return true;
            }

            return false;
        }

        private 后台任务 raWorker;

        private void btnReadAllCards_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                if (lbxFiles.Items == null || lbxFiles.Items.Count <= 0)
                {
                    LMessageBox.ShowWarning("请先载入要识别的答题卡文件（或目录）");
                    return;
                }

                if (cmbPathOfAnswerSheetDefinitionFile.SelectedIndex < 0)
                {
                    LMessageBox.ShowWarning("请先选择要使用的答题卡定义文件！");
                    return;
                }

                imgPreviewSource.Source = null;
                imgPreview.Source = null;
                lbxFiles.SelectedIndex = -1;

                var 精确匹配模式 = ckxStrictMatchingMode.IsChecked.HasValue && ckxStrictMatchingMode.IsChecked.Value;
                var 是否带批改信息 = ckxWithCorrectInfo.IsChecked.HasValue && ckxWithCorrectInfo.IsChecked.Value;
                var 用冒号分隔答案 = ckxSplitBetweenQuestions.IsChecked.HasValue && ckxSplitBetweenQuestions.IsChecked.Value;
                var autoConnectOutBorder = ckxAutoConnectOutBorder.IsChecked.HasValue && ckxAutoConnectOutBorder.IsChecked.Value;

                // 同步待处理列表识别状态
                if (lbxFiles.Items != null && filesList != null && lbxFiles.Items.Count == filesList.Count)
                {
                    for (int i = 0; i < lbxFiles.Items.Count; i++)
                    {
                        var li = lbxFiles.Items[i] as FileListBoxItem;
                        if (li == null) continue;

                        filesList[i].识别状态 = li.识别状态;
                    }
                }

                var valTxt1 = sliderOfMinMatchRateForQuestions.Value.ToString();
                tbMinMatchRateForQuestions.Text = valTxt1 + "%";
                App.WorkspaceConfigManager.Set("MinRateOfAnswerSheetQuestions", valTxt1);

                var valTxt2 = sliderOfMinMatchRateForNumbers.Value.ToString();
                tbMinMatchRateForNumbers.Text = valTxt2 + "%";
                App.WorkspaceConfigManager.Set("MinRateOfAnswerSheetNumbers", valTxt2);

                var cmi = cmbPathOfAnswerSheetDefinitionFile.SelectedItem as AnswerSheetDefinitionFileItem;
                if (cmi == null)
                {
                    LMessageBox.ShowWarning("请先选择要使用的答题卡定义文件！");
                    return;
                }

                var ad = new 答题卡定义(System.IO.File.ReadAllText(cmi.FilePath))
                {
                    精确匹配模式 = 精确匹配模式,
                    是否带批改信息 = 是否带批改信息,
                    用冒号分隔答案 = 用冒号分隔答案,
                    参考答案字符串 = tbxSetAnswer.Text.ToUpper(),
                    待处理文件列表 = filesList,
                    编号最小匹配比例 = sliderOfMinMatchRateForNumbers.Value,
                    试题最小匹配比例 = sliderOfMinMatchRateForQuestions.Value,
                    读卡时尝试连接外框缺口 = autoConnectOutBorder,
                };

                tbxResult.Text = "";  // 这才需要清空

                // 这里用多任务处理，防止界面卡死
                raWorker = new 后台任务();
                //异步取消 需要增加这个 不然取消失效
                raWorker.是否支持取消任务 = true;
                //支持报告进度
                raWorker.是否支持报告任务进度 = true;
                //注册滚动条事件
                raWorker.任务进度报告函数 += RaWorker_任务进度报告函数;

                //注册任务
                raWorker.任务执行函数 += RaWorker_任务执行函数;
                //任务完毕触发
                raWorker.任务完成函数 += RaWorker_任务完成函数;

                // 开始任务前，禁止学生花名册修改
                tbxStudentNames.IsEnabled = false;
                LoadStudents();

                //给任务传参
                raWorker.开始任务(ad);

                btnReadAllCards.IsEnabled = false;

            }
            catch (Exception ex)
            {
                LMessageBox.ShowWarning(ex.Message);
            }
        }

        private void LoadStudents()
        {
            students.Clear();

            var lines = tbxStudentNames.Text.Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries);
            foreach (var line in lines)
            {
                var spans = line.Split(new char[] { '\t' }, StringSplitOptions.RemoveEmptyEntries);
                if (spans.Length != 2) continue;

                if (int.TryParse(spans[0], out int number))
                {
                    students.Add(new Student() { Number = number.ToString(), Name = spans[1], });
                }
            }
        }

        private List<Student> students = new List<Student>();

        private void RaWorker_任务完成函数(object sender, 任务完成事件参数 e)
        {
            if (sender is BackgroundWorker)
            {
                if (e.参数体.Cancelled == true)
                {
                    LMessageBox.ShowWarning("已取消继续识别答题卡。");
                }

                后台任务 k = (后台任务)sender;
                k.置任务输出信息行($"完成识别答题卡操作。请检查输出信息是否准确...");

                k.任务执行函数 -= RaWorker_任务执行函数;
                k.任务完成函数 -= RaWorker_任务完成函数;
                k = null;
                btnReadAllCards.IsEnabled = true;

                // 结束任务后，允许学生花名册修改
                tbxStudentNames.IsEnabled = true;
            }
        }

        private void RaWorker_任务执行函数(object sender, 任务执行事件参数 e)
        {
            var ad = e.参数值 as 答题卡定义;
            if (ad == null)
            {
                LMessageBox.ShowWarning("未传入答题卡定义对象。");
                e.是否取消 = true;
                return;
            }

            if (ad.边框宽度比 == null || ad.边框宽度比.HasValue == false || ad.边框高度比 == null || ad.边框高度比.HasValue == false)
            {
                LMessageBox.ShowWarning("答题卡定义有问题，未找到“边框宽度比”或“边框高度比”的值，无法进行切边操作。请使用最新版 LME 导出答题卡定义文本文件。");
                e.是否取消 = true;
                return;
            }

            if (ad.编号条目列表 == null || ad.编号条目列表.Count < 1)
            {
                LMessageBox.ShowWarning("答题卡定义有问题，未找到“编号”填涂定义，无法判断是谁的成绩。");
                e.是否取消 = true;
                return;
            }

            if (ad.试题条目列表 == null || ad.试题条目列表.Count < 1)
            {
                LMessageBox.ShowWarning("答题卡定义有问题是，未找到试题填涂定义，无法读取。");
                e.是否取消 = true;
                return;
            }

            var answer = ad.参考答案字符串;
            if (answer != null) answer = answer.Replace(" ", "").Replace("　", "").Replace("\t", "");

            // 既可以直接在答题卡设计时设置每题的参考答案；也可以复用答题卡，每次批改时按新的参考答案来批阅。
            if (string.IsNullOrEmpty(answer) == false)
            {
                // 设置了答案字符串
                if (answer.Contains(",") || answer.Contains("，") || answer.Contains(";") || answer.Contains("；"))
                {
                    // 不定项选择题或多项选择题
                    // 答案以分号、逗号分隔（如无分号逗号则视为单选题，如无答案则以答题卡定义文件中的答案为准）
                    // 分号用以分隔试题，逗号用以分开题号与答案。

                    var quesSpans = answer.Split(new char[] { ';', '；' }, StringSplitOptions.RemoveEmptyEntries);

                    for (int i = 0; i < quesSpans.Length; i++)
                    {
                        var qSpan = quesSpans[i];
                        if (qSpan.Contains(",") || qSpan.Contains("，"))
                        {
                            // 指定题号
                            var optionSpans = qSpan.Split(new char[] { ',', '，' }, StringSplitOptions.RemoveEmptyEntries);
                            if (optionSpans.Length >= 2)
                            {
                                var ques = ad.取试题条目(optionSpans[0]);
                                if (ques == null)
                                {
                                    LMessageBox.ShowWarning($"第{i + 1}题给定的题号不存在，无法继续。");
                                    return;
                                }

                                ques.置答案文本(optionSpans[1].ToUpper());
                            }
                            else
                            {
                                // 实际上未提供题号，出错。
                                LMessageBox.ShowWarning($"第{i + 1}题写了逗号，但未给出题号或答案，无法继续。");
                                return;
                            }
                        }
                        else
                        {
                            // 自动题号
                            if (i < ad.试题条目列表.Count)
                            {
                                ad.试题条目列表[i].置答案文本(qSpan.ToUpper());
                            }
                        }
                    }
                }
                else
                {
                    // 单选题
                    if (ad.试题条目列表 != null && ad.试题条目列表.Count > 0)
                    {
                        if (answer.Length > ad.试题条目列表.Count)
                        {
                            LMessageBox.ShowWarning("答案字符串过长！");
                        }

                        for (int i = 0; i < answer.Length; i++)
                        {
                            if (ad.试题条目列表[i].填涂选项集 == null || ad.试题条目列表[i].填涂选项集.Count < 0) continue;

                            foreach (var o in ad.试题条目列表[i].填涂选项集)
                            {
                                o.置是否答案(false);
                            }

                            for (int j = 0; j < ad.试题条目列表[i].填涂选项集.Count; j++)
                            {
                                var option = ad.试题条目列表[i].填涂选项集[j];
                                if (option != null && option.选项文本 != null && option.选项文本.ToUpper() == answer[i].ToString().ToUpper())
                                {
                                    option.置是否答案(true);
                                }
                            }
                        }
                    }
                }
            }// 未设置，以答题卡定义中的答案为准

            后台任务 k = (后台任务)sender;

            for (int i = 0; i < ad.待处理文件列表.Count; i++)
            {
                if (k.CancellationPending)
                {
                    return;
                }

                Mat srcMat = null;
                Mat readResultMat = null;

                var answerSheetFile = ad.待处理文件列表[i];

                // if (answerSheetFile == null || answerSheetFile.识别状态 == 识别状态.Finish) continue;
                // 虽然说无须重复识别，但这样做会导致一些意外的错误，没有必要省这点算力。

                var numbersList = new List<string>();
                var answersList = new List<string>();

                readResultMat = ReadAnswerSheet.匹配答案(answerSheetFile.FilePath, ad, ref numbersList, ref answersList,
                                                         out int fzfgCount, ad.精确匹配模式, ad.是否带批改信息, 定位模式.外框模式, ref srcMat);

                answerSheetFile.鸟瞰图 = srcMat;
                answerSheetFile.结果图 = readResultMat;

                var numSB = new StringBuilder();
                var ansSB = new StringBuilder();
                foreach (var x in numbersList)
                {
                    numSB.Append(x);
                }

                foreach (var y in answersList)
                {
                    ansSB.Append(y + (ad.用冒号分隔答案 ? "; " : ""));
                }

                answerSheetFile.识别编号 = numSB.ToString();
                answerSheetFile.识别答案 = ansSB.ToString();

                // 判断识别状态
                // 基本规则：
                //     1. 无论是不定项、多项还是单项，都检查
                //     2. 先检查是否每个编号都只填涂了一位数字
                //     3. 再检查是否道题至少填涂了一个答案
                //     4. 未填涂任何方块者返回错误，有缺失的为存疑
                if (ad.答题卡宽高比.HasValue && readResultMat != null)
                {
                    // 判断宽高比
                    var realRate = (double)readResultMat.Width / readResultMat.Height;

                    // 判断宽高比
                    if (realRate < ad.答题卡宽高比 - 0.4 || realRate > ad.答题卡宽高比 + 0.4)
                    {
                        answerSheetFile.识别状态 = 识别状态.ER;
                        answerSheetFile.错误信息 += "答题卡宽高比异常幅度大于 0.4。";
                    }
                    else if (realRate < ad.答题卡宽高比 - 0.2 || realRate > ad.答题卡宽高比 + 0.2)
                    {
                        answerSheetFile.识别状态 = 识别状态.WR;
                        answerSheetFile.错误信息 += "答题卡宽高比异常幅度大于 0.2。";
                    }
                    else
                    {
                        if ((ad.答题卡宽高比 > 1 && realRate < 1) || (ad.答题卡宽高比 < 1 && realRate > 1))
                        {
                            answerSheetFile.识别状态 = 识别状态.WR;
                            answerSheetFile.错误信息 += "答题卡宽高比存在小幅度倒错。";
                        }
                    }
                }

                if (answerSheetFile.识别编号.Contains("$") || answerSheetFile.识别答案.Contains("$"))
                {
                    answerSheetFile.识别状态 = 识别状态.ER;
                    answerSheetFile.错误信息 += "有不能识别的编号或答案。";
                }

                if (answerSheetFile.识别编号.Length != ad.编号条目列表.Count)
                {
                    answerSheetFile.识别状态 = 识别状态.ER;
                    answerSheetFile.错误信息 += "识别的编号长度不正确。";
                }

                if (answerSheetFile.是否有空值 || answerSheetFile.答案数目 != ad.试题条目列表.Count)
                {
                    answerSheetFile.识别状态 = 识别状态.WR;
                    answerSheetFile.错误信息 += "未识别到答案或识别到的答案数目不符。";
                }

                if (是否全空列表(numbersList) && 是否全空列表(answersList))
                {
                    answerSheetFile.识别状态 = 识别状态.ER;
                    answerSheetFile.错误信息 += "未识别到编号或未识别到答案。";
                }

                if (是否含空元素列表(numbersList) && 是否含非空元素列表(numbersList) &&
                         是否含空元素列表(answersList) && 是否含非空元素列表(answersList))
                {
                    answerSheetFile.识别状态 = 识别状态.WR;
                    answerSheetFile.错误信息 += "部分编号位或答案位未能识别。";
                }

                if (numbersList.Count == ad.编号条目列表.Count && answersList.Count == ad.试题条目列表.Count &&
                         是否全非空列表(numbersList) && 是否全非空列表(answersList))
                {
                    answerSheetFile.识别状态 = 识别状态.FN;
                    answerSheetFile.错误信息 = "";
                }

                if (answerSheetFile.识别答案.Contains(";") == false && answerSheetFile.识别答案.Length != ad.试题条目列表.Count)
                {
                    answerSheetFile.识别状态 = 识别状态.WR;
                    answerSheetFile.错误信息 += "识别到的答案数目不符。";
                }

                raWorker.ReportProgress(i + 1, answerSheetFile);
            }
        }

        private Student FindStudent(string number)
        {
            if (string.IsNullOrWhiteSpace(number)) return null;

            number = number.TrimStart(new char[] { ' ', '　', '\t', '0' });

            if (int.TryParse(number, out var studentNumber))
            {
                foreach (var stu in students)
                {
                    if (stu.Number.ToLower() == studentNumber.ToString().ToLower())
                    {
                        return stu;
                    }
                }

                return null;
            }
            else return null;
        }

        private void RaWorker_任务进度报告函数(object sender, 任务进度事件参数 e)
        {
            var task = sender as 后台任务;

            task.置任务进度(e.参数体.ProgressPercentage);
            var ad = e.参数体.UserState as AnswerSheetFile;
            task.置任务输出信息行($"正在识别：{ad.Header}...");

            var i = e.参数体.ProgressPercentage - 1;
            if (i >= 0 && i < lbxFiles.Items.Count)
            {
                var destItem = lbxFiles.Items[i] as FileListBoxItem;
                if (destItem != null)
                {
                    destItem.UpdateFromAnswerSheetFile(ad);

                    var stu = FindStudent(destItem.识别编号);
                    if (stu == null)
                    {
                        tbxResult.AppendText($"（{new string('*', destItem.识别编号.Length)}）\t<未知>\t{i + 1}.\t{destItem.Header}\t{destItem.识别状态}\t考号：\t（{destItem.识别编号}）\t{destItem.识别答案}\r\n");
                    }
                    else
                    {
                        tbxResult.AppendText($"（{stu.Number}）\t{stu.Name}\t{i + 1}.\t{destItem.Header}\t{destItem.识别状态}\t考号：\t（{destItem.识别编号}）\t{destItem.识别答案}\r\n");
                    }
                }
            }

            if (Globals.MainWindow.rdFindAndReplace.ActualHeight <= 20)
            {
                Globals.MainWindow.rdFindAndReplace.Height = new GridLength(0, GridUnitType.Auto);
                Globals.MainWindow.cmbFindText.UpdateLayout();
                Globals.MainWindow.tcFindAndReplace.SelectedIndex = 2;
            }
        }

        /// <summary>
        /// 只支持 jpg、jpeg、png 三种。gif、bmp、tiff 被去除了。
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        public static List<string> GetImageFiles(string path)
        {
            List<string> imagePaths = new List<string>();
            string[] validExtensions = { ".jpg", ".jpeg", ".png", ".tiff", ".bmp" };

            try
            {
                foreach (string file in Directory.GetFiles(path))
                {
                    if (validExtensions.Contains(System.IO.Path.GetExtension(file).ToLower()))
                    {
                        imagePaths.Add(file);
                    }
                }

                foreach (string dir in Directory.GetDirectories(path))
                {
                    imagePaths.AddRange(GetImageFiles(dir));
                }
            }
            catch (Exception ex)
            {
                LMessageBox.ShowWarning("Error: " + ex.Message);
            }

            return imagePaths;
        }

        public static List<string> GetDefinitionFiles(string path)
        {
            List<string> defPaths = new List<string>();
            string[] validExtensions = { ".lmasd", };

            try
            {
                foreach (string file in Directory.GetFiles(path))
                {
                    if (validExtensions.Contains(System.IO.Path.GetExtension(file).ToLower()))
                    {
                        defPaths.Add(file);
                    }
                }

                foreach (string dir in Directory.GetDirectories(path))
                {
                    defPaths.AddRange(GetImageFiles(dir));
                }
            }
            catch (Exception ex)
            {
                LMessageBox.ShowWarning("Error: " + ex.Message);
            }

            return defPaths;
        }

        private void btnLoadPathOfAnswerSheetDefinitionFile_Click(object sender, RoutedEventArgs e)
        {
            #region 此代码已不需要
            //System.Windows.Forms.OpenFileDialog opnDialogue = new System.Windows.Forms.OpenFileDialog() { Title = "请选择 LME 导出的答题卡定义文件：" };
            //opnDialogue.Filter = "报表定义(*.lmasd)|*.lmasd";
            //if (opnDialogue.ShowDialog() == System.Windows.Forms.DialogResult.OK)
            //{
            //    bool hasPath = false;
            //    for (int i = 0; i < this.cmbPathOfAnswerSheetDefinitionFile.Items.Count; i++)
            //    {
            //        if (this.cmbPathOfAnswerSheetDefinitionFile.Items[i].ToString().ToLower() == opnDialogue.FileName.ToLower())
            //        {
            //            this.cmbPathOfAnswerSheetDefinitionFile.SelectedIndex = i;
            //            hasPath = true;
            //            break;
            //        }
            //    }

            //    if (hasPath == false)
            //    {
            //        this.cmbPathOfAnswerSheetDefinitionFile.Items.Add(opnDialogue.FileName);
            //        this.cmbPathOfAnswerSheetDefinitionFile.SelectedIndex = this.cmbPathOfAnswerSheetDefinitionFile.Items.Count - 1;
            //    }
            //}
            #endregion

            LoadAnswerSheetDefinitionFiles();
        }

        private void btnLoadPathOfAnswerSheetsDir_Click(object sender, RoutedEventArgs e)
        {
            var dialog = new CommonOpenFileDialog() { Multiselect = false, };
            dialog.IsFolderPicker = true;//设置为选择文件夹
            dialog.Title = "请选择答题卡照片所在目录";
            if (dialog.ShowDialog() != CommonFileDialogResult.Ok) return;  // 用户取消操作

            this.tbxPathOfAnswerSheetFileOrDir.Text = dialog.FileName;

            if (string.IsNullOrEmpty(this.tbxPathOfAnswerSheetFileOrDir.Text))
            {
                var vb = new VisualBrush();
                var tb = new TextBlock() { Text = "单个答题卡照片文件路径或照片所在目录路径", Foreground = System.Windows.Media.Brushes.Gray, };
                vb.Visual = tb;
                vb.Stretch = Stretch.None;
                tbxPathOfAnswerSheetFileOrDir.Background = vb;
            }
            else
            {
                tbxPathOfAnswerSheetFileOrDir.Background = System.Windows.Media.Brushes.White;
            }

            ListFiles();
        }

        /// <summary>
        /// 非界面列表，长度总是与界面上的列表一致。
        /// </summary>
        private List<AnswerSheetFile> filesList = new List<AnswerSheetFile>();

        private void ListFiles()
        {
            // 显示这些文件
            lbxFiles.Items.Clear();
            filesList.Clear();

            tbxResult.Text = "";
            tbsResultOfSelectedItem.Text = "";
            tbxErrMsg.Text = "";
            imgPreviewSource.Source = null;
            imgPreview.Source = null;

            if (Directory.Exists(tbxPathOfAnswerSheetFileOrDir.Text))
            {
                var fileEntries = GetImageFiles(tbxPathOfAnswerSheetFileOrDir.Text);
                for (int i = 0; i < fileEntries.Count; i++)
                {
                    var fe = fileEntries[i];
                    var fi = new FileListBoxItem(this)
                    {
                        索引 = (i + 1).ToString(),
                        FilePath = fe,  // 注意这个顺序
                    };
                    lbxFiles.Items.Add(fi);  // 这里得到的只会是支持的图像
                    filesList.Add(new AnswerSheetFile(fe));  // 这里之所以需要两个列表，是因为多线程处理不能使用界面线程元素。所以这里有两个长度一致的列表。
                }
            }

            if (lbxFiles.Items.Count > 0)
            {
                lbxFiles.SelectedIndex = 0;
            }
        }

        private void tbxPathOfAnswerSheetsDir_KeyDown(object sender, System.Windows.Input.KeyEventArgs e)
        {
            if (string.IsNullOrWhiteSpace(tbxPathOfAnswerSheetFileOrDir.Text)) return;

            if (e.Key == System.Windows.Input.Key.Enter)
            {
                if (System.IO.File.Exists(tbxPathOfAnswerSheetFileOrDir.Text) || System.IO.Directory.Exists(tbxPathOfAnswerSheetFileOrDir.Text))
                {
                    BtnReadSelectedItem_Click(sender, e);
                }
                else
                {
                    if (Directory.Exists(tbxPathOfAnswerSheetFileOrDir.Text))
                    {
                        ListFiles();
                    }
                }
            }
        }

        private void tbxPathOfAnswerSheetsDir_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
        {
            if (string.IsNullOrEmpty(tbxPathOfAnswerSheetFileOrDir.Text))
            {
                var vb = new VisualBrush();
                var tb = new TextBlock() { Text = "单个答题卡照片文件路径或照片所在目录路径", Foreground = System.Windows.Media.Brushes.Gray, };
                vb.Visual = tb;
                vb.Stretch = Stretch.None;
                tbxPathOfAnswerSheetFileOrDir.Background = vb;
            }
            else
            {
                tbxPathOfAnswerSheetFileOrDir.Background = System.Windows.Media.Brushes.White;
            }
        }

        private void btnLoadPathOfAnswerSheetFile_Click(object sender, RoutedEventArgs e)
        {
            System.Windows.Forms.OpenFileDialog opnDialogue = new System.Windows.Forms.OpenFileDialog() { Title = "请选择单个答题卡照片：" };
            opnDialogue.Filter = "图像文件(*.jpg;*.jpge;*.png;*.bmp;*.tiff)|*.jpg;*.jpge;*.png;*.bmp;*.tiff";
            if (opnDialogue.ShowDialog() == System.Windows.Forms.DialogResult.OK)
            {
                this.tbxPathOfAnswerSheetFileOrDir.Text = opnDialogue.FileName;

                lbxFiles.Items.Clear();
                filesList.Clear();  // 两个列表是防止线程冲突

                lbxFiles.Items.Add(new FileListBoxItem(this) { 索引 = "1", FilePath = opnDialogue.FileName, });
                filesList.Add(new AnswerSheetFile(opnDialogue.FileName));

                lbxFiles.SelectedIndex = 0;

                if (string.IsNullOrEmpty(tbxPathOfAnswerSheetFileOrDir.Text))
                {
                    var vb = new VisualBrush();
                    var tb = new TextBlock() { Text = "单个答题卡照片文件路径或照片所在目录路径", Foreground = System.Windows.Media.Brushes.Gray, };
                    vb.Visual = tb;
                    vb.Stretch = Stretch.None;
                    tbxPathOfAnswerSheetFileOrDir.Background = vb;
                }
                else
                {
                    tbxPathOfAnswerSheetFileOrDir.Background = System.Windows.Media.Brushes.White;
                }
            }
        }

        private void cmbPathOfAnswerSheetDefinitionFile_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (string.IsNullOrEmpty(cmbPathOfAnswerSheetDefinitionFile.SelectedItem.ToString()))
            {
                var vb = new VisualBrush();
                var tb = new TextBlock() { Text = "此文件应系由报表设计器预览时自动导出到工作区 $_Assets~ 目录下", Foreground = System.Windows.Media.Brushes.Gray, };
                vb.Visual = tb;
                vb.Stretch = Stretch.None;
                cmbPathOfAnswerSheetDefinitionFile.Background = vb;
            }
            else
            {
                cmbPathOfAnswerSheetDefinitionFile.Background = System.Windows.Media.Brushes.White;
            }
        }

        private void lbxFiles_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            // 这里不应该识别，只应切换识别结果

            if (lbxFiles.Items == null || lbxFiles.Items.Count <= 0 || lbxFiles.SelectedIndex < 0) return;
            var fitem = lbxFiles.SelectedItem as FileListBoxItem;
            if (fitem == null) return;
            RefreshResultOfSelectedItem(fitem);
        }

        private void RefreshResultOfSelectedItem(FileListBoxItem fitem)
        {
            if (fitem.识别状态 != 识别状态.BE)
            {
                imgPreview.Source = MatToBitmapImage(fitem.结果图);
                imgPreviewSource.Source = MatToBitmapImage(fitem.鸟瞰图);
                tbsResultOfSelectedItem.Text = $"学号：{fitem.识别编号}\t答案：{fitem.识别答案}";
                tbxErrMsg.Text = fitem.错误信息;
            }
            else
            {
                imgPreview.Source = null;
                imgPreviewSource.Source = null;
                tbsResultOfSelectedItem.Text = "";
                tbxErrMsg.Text = "";
            }
        }

        private void btnCancelReadAllCards_Click(object sender, RoutedEventArgs e)
        {
            if (raWorker != null)
            {
                raWorker.取消任务();
            }
        }

        private void btnRefreshReadResult_Click(object sender, RoutedEventArgs e)
        {
            if (lbxFiles == null || lbxFiles.Items == null || lbxFiles.Items.Count <= 0)
            {
                LMessageBox.ShowWarning("未载入答题卡照片文件。");
                return;
            }

            var 识别结果列表 = new List<识别结果>();
            var sb = new StringBuilder();
            for (int index = 0; index < lbxFiles.Items.Count - 1; index++)
            {
                var li = lbxFiles.Items[index];
                var fitem = li as FileListBoxItem;
                if (fitem == null) continue;

                识别结果列表.Add(new 识别结果() { 识别编号 = fitem.识别编号, 识别答案文本 = fitem.识别答案 });

                var stu = FindStudent(fitem.识别编号);
                if (stu != null)
                {
                    sb.Append($"（{stu.Number}）\t{stu.Name}\t{index + 1}.\t{fitem.Header}\t{fitem.识别状态}\t（{fitem.识别编号}）\t{fitem.识别答案}\r\n");
                }
                else
                {
                    sb.Append($"（{new string('*', fitem.识别编号.Length)}）\t<未知>\t{index + 1}.\t{fitem.Header}\t{fitem.识别状态}\t（{fitem.识别编号}）\t{fitem.识别答案}\r\n");
                }
            }

            var lastLi = lbxFiles.Items[lbxFiles.Items.Count - 1] as FileListBoxItem;
            if (lastLi != null)
            {
                识别结果列表.Add(new 识别结果() { 识别编号 = lastLi.识别编号, 识别答案文本 = lastLi.识别答案 });

                var stu = FindStudent(lastLi.识别编号);
                if (stu != null)
                {
                    sb.Append($"（{stu.Number}）\t{stu.Name}\t{lbxFiles.Items.Count}.\t{lastLi.Header}\t{lastLi.识别状态}\t（{lastLi.识别编号}）\t{lastLi.识别答案}\r\n");
                }
                else
                {
                    sb.Append($"（{new string('*', lastLi.识别编号.Length)}）\t<未知>\t{lbxFiles.Items.Count}.\t{lastLi.Header}\t{lastLi.识别状态}\t（{lastLi.识别编号}）\t{lastLi.识别答案}\r\n");
                }
            }

            tbxResult.Text = sb.ToString();

            if (students == null || students.Count == 0)
            {
                LoadStudents();  // 再给次机会提供名册。
            }

            if (students != null && students.Count > 0)
            {
                var resultSb = new StringBuilder();

                foreach (var stu in students)
                {
                    var readerItem = 匹配识别结果(stu.Number, ref 识别结果列表);
                    if (readerItem != null)
                    {
                        resultSb.Append($"{stu.Number}\t{stu.Name}\t{readerItem.识别答案文本}\r\n");
                    }
                    else
                    {
                        resultSb.Append($"{stu.Number}\t{stu.Name}\t{"<--缺卡-->"}\r\n");
                    }
                }

                LMessageBox.Show(resultSb.ToString(), Globals.AppName, MessageBoxButton.OK, MessageBoxImage.Information, "", null, this);
            }
            else
            {
                LMessageBox.Show("未提供学生名册，此处不会显示对应学生答题情况，请自行处理识别信息。", Globals.AppName, MessageBoxButton.OK, MessageBoxImage.Information, "", null, this);
            }
        }

        private 识别结果 匹配识别结果(string numberTxt, ref List<识别结果> list)
        {
            if (string.IsNullOrWhiteSpace(numberTxt)) return null;

            if (list == null || list.Count <= 0) return null;

            foreach (var readerItem in list)
            {
                if (numberTxt.ToLower().TrimStart(new char[] { '0' }) == readerItem.识别编号.ToLower().TrimStart(new char[] { '0' }))
                {
                    return readerItem;
                }
            }

            return null;
        }

        private void miLocateAtExplorer_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                if (lbxFiles.SelectedItem == null) return;
                var selItem = lbxFiles.SelectedItem as FileListBoxItem;
                if (selItem == null) return;

                if (File.Exists(selItem.FilePath))
                {
                    var file = new 文件(selItem.FilePath);
                    file.在文件管理器中定位();
                }
            }
            catch (Exception ex)
            {
                LMessageBox.Show(ex.Message);
            }
        }

        private void miCorrectByUser_Click(object sender, RoutedEventArgs e)
        {
            if (lbxFiles.SelectedItem == null) return;
            var selItem = lbxFiles.SelectedItem as FileListBoxItem;
            if (selItem == null) return;

            var fInfo = EntryInfoInputBox.显示双值输入框("请输入手工批阅编号：", selItem.识别编号, "请输入手工批阅答案：",
                                                         selItem.识别答案, "全半角空格、Tab 字符会被忽略", $"手工批阅{selItem.Header}", true);

            if (fInfo != null && fInfo.Count == 2)
            {
                selItem.识别编号 = fInfo[0].ToString().Replace(" ", "").Replace("　", "").Replace("\t", "");
                selItem.识别答案 = fInfo[1].ToString().Replace(" ", "").Replace("　", "").Replace("\t", "");
                selItem.识别状态 = 识别状态.FN;

                RefreshResultOfSelectedItem(selItem);
            }
        }

        private void miReCorrectSelectedItem_Click(object sender, RoutedEventArgs e)
        {
            BtnReadSelectedItem_Click(sender, e);
        }

        private void MetroWindow_PreviewKeyDown(object sender, KeyEventArgs e)
        {
            KeyStates ksRShift = Keyboard.GetKeyStates(Key.RightShift);
            KeyStates ksLShift = Keyboard.GetKeyStates(Key.LeftShift);
            KeyStates ksLAlt = Keyboard.GetKeyStates(Key.LeftAlt);
            KeyStates ksRAlt = Keyboard.GetKeyStates(Key.RightAlt);
            KeyStates ksLCtrl = Keyboard.GetKeyStates(Key.LeftCtrl);
            KeyStates ksRCtrl = Keyboard.GetKeyStates(Key.RightCtrl);

            bool isCtrl, isShift, isAlt;

            isShift = (ksLShift & KeyStates.Down) > 0 || (ksRShift & KeyStates.Down) > 0;
            isCtrl = (ksLCtrl & KeyStates.Down) > 0 || (ksRCtrl & KeyStates.Down) > 0;
            isAlt = (ksLAlt & KeyStates.Down) > 0 || (ksRAlt & KeyStates.Down) > 0;

            switch (e.Key)
            {
                case Key.F1:
                    {
                        miReLoadImageAndCorrectByUser_Click(sender, e);
                        break;
                    }
                case Key.F2:
                    {
                        if (lbxFiles.SelectedItem != null)
                        {
                            miCorrectByUser_Click(sender, e);
                            e.Handled = true;
                        }
                        break;
                    }
                case Key.L:
                    {
                        if (isCtrl)
                        {
                            if (lbxFiles.SelectedItem != null)
                            {
                                miLocateAtExplorer_Click(sender, e);
                                e.Handled = true;
                            }
                        }
                        break;
                    }
                case Key.R:
                    {
                        if (isCtrl)
                        {
                            if (lbxFiles.SelectedItem != null)
                            {
                                BtnReadSelectedItem_Click(sender, e);
                                e.Handled = true;
                            }
                        }
                        break;
                    }
            }
        }

        private void miReLoadImageAndCorrectByUser_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                if (lbxFiles.SelectedItem == null) return;
                var selItem = lbxFiles.SelectedItem as FileListBoxItem;
                if (selItem == null) return;

                if (File.Exists(selItem.FilePath))
                {
                    var srcMat = new Mat(selItem.FilePath);
                    selItem.鸟瞰图 = srcMat;
                    imgPreviewSource.Source = MatToBitmapImage(srcMat);
                    miCorrectByUser_Click(sender, e);
                }
            }
            catch (Exception ex)
            {
                LMessageBox.Show(ex.Message);
            }
        }

        private void sliderOfMinMatchRateForNumbers_DragCompleted(object sender, System.Windows.Controls.Primitives.DragCompletedEventArgs e)
        {
            if (tbMinMatchRateForNumbers != null)
            {
                tbMinMatchRateForNumbers.Text = sliderOfMinMatchRateForNumbers.Value.ToString() + "%";

                if (System.IO.File.Exists(cmbPathOfAnswerSheetDefinitionFile.SelectedIndex.ToString()))
                {
                    BtnReadSelectedItem_Click(sender, e);
                }
            }
        }

        private void sliderOfMinMatchRateForQuestions_DragCompleted(object sender, System.Windows.Controls.Primitives.DragCompletedEventArgs e)
        {
            if (tbMinMatchRateForQuestions != null)
            {
                tbMinMatchRateForQuestions.Text = sliderOfMinMatchRateForQuestions.Value.ToString() + "%";

                if (System.IO.File.Exists(cmbPathOfAnswerSheetDefinitionFile.SelectedIndex.ToString()))
                {
                    BtnReadSelectedItem_Click(sender, e);
                }
            }
        }

        private void sliderOfMinMatchRateForNumbers_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            if (tbMinMatchRateForNumbers != null)
            {
                tbMinMatchRateForNumbers.Text = sliderOfMinMatchRateForNumbers.Value.ToString() + "%";
            }
        }

        private void sliderOfMinMatchRateForQuestions_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            if (tbMinMatchRateForQuestions != null)
            {
                tbMinMatchRateForQuestions.Text = sliderOfMinMatchRateForQuestions.Value.ToString() + "%";
            }
        }

        private void btnHelp_Click(object sender, RoutedEventArgs e)
        {
            var bookFullName = Globals.InstalledPath + "answer_sheet_reader.chm";
            if (File.Exists(bookFullName))
            {
                System.Diagnostics.Process.Start(bookFullName);
            }
        }

        private void tbxSetAnswer_TextChanged(object sender, TextChangedEventArgs e)
        {
            if (string.IsNullOrEmpty(tbxSetAnswer.Text))
            {
                var vb = new VisualBrush();
                var tb = new TextBlock() { Text = "请输入答案字符串（可以带空格，不分大小写）", Foreground = System.Windows.Media.Brushes.Gray, };
                vb.Visual = tb;
                vb.Stretch = Stretch.None;
                tbxSetAnswer.Background = vb;
            }
            else
            {
                tbxSetAnswer.Background = System.Windows.Media.Brushes.White;
            }
        }
    }


    public class FileListBoxItem : ListBoxItem
    {
        public FileListBoxItem(AnswerSheetReader masterReaderWindow)
        {
            this._主读卡窗口 = masterReaderWindow;
            Initialize();

            this.PreviewMouseLeftButtonDown += FileListBoxItem_PreviewMouseLeftButtonDown;
        }

        public string 索引 { get; set; } = "";

        private void FileListBoxItem_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            if (this._主读卡窗口 == null) return;

            if (e.ClickCount == 2)  // 双击才应重新识别
            {
                if (this._主读卡窗口.lbxFiles.Items.Contains(this))
                {
                    this._主读卡窗口.lbxFiles.SelectedItem = this;
                    this._主读卡窗口.BtnReadSelectedItem_Click(sender, e);
                }
            }
            //else // 单击只应呈现其状态
        }

        private AnswerSheetReader _主读卡窗口;

        public AnswerSheetReader 主读卡窗口 { get { return _主读卡窗口; } }

        private void Initialize()
        {
            headerTextBlock.Width = this.FontSize;
            basePanel.Orientation = Orientation.Horizontal;
            basePanel.Children.Add(headerTextBlock);
            basePanel.Children.Add(contentTextBlock);
            this.Content = basePanel;

            this.Style = this.FindResource("MetroListBoxItem") as System.Windows.Style;
        }

        private void 更新识别状态()
        {
            switch (_识别状态)
            {
                case 识别状态.BE:
                    {
                        headerTextBlock.Text = "-";
                        headerTextBlock.Foreground = System.Windows.Media.Brushes.Black;
                        headerTextBlock.Background = System.Windows.Media.Brushes.Transparent;
                        break;
                    }
                case 识别状态.ER:
                    {
                        headerTextBlock.Text = "!";
                        headerTextBlock.Foreground = System.Windows.Media.Brushes.White;
                        headerTextBlock.Background = System.Windows.Media.Brushes.Red;
                        break;
                    }
                case 识别状态.WR:
                    {
                        headerTextBlock.Text = "?";
                        headerTextBlock.Foreground = System.Windows.Media.Brushes.White;
                        headerTextBlock.Background = System.Windows.Media.Brushes.Gold;
                        break;
                    }
                case 识别状态.FN:
                    {
                        headerTextBlock.Text = "+";
                        headerTextBlock.Foreground = System.Windows.Media.Brushes.White;
                        headerTextBlock.Background = System.Windows.Media.Brushes.Green;
                        break;
                    }
            }
        }

        internal void UpdateFromAnswerSheetFile(AnswerSheetFile answerSheetFile)
        {
            this.鸟瞰图 = answerSheetFile.鸟瞰图;
            this.结果图 = answerSheetFile.结果图;
            this.识别编号 = answerSheetFile.识别编号;
            this.识别答案 = answerSheetFile.识别答案;
            this.错误信息 = answerSheetFile.错误信息;
            this.识别状态 = answerSheetFile.识别状态;
            this.更新识别状态();
        }

        public FileListBoxItem(string filePath)
        {
            Initialize();
            FilePath = filePath;
        }

        public FileListBoxItem(string filePath, Mat 鸟瞰图, Mat 结果图)
        {
            Initialize();
            this.鸟瞰图 = 鸟瞰图;
            this.结果图 = 结果图;
            FilePath = filePath;
        }

        private StackPanel basePanel = new StackPanel();

        private TextBlock headerTextBlock = new TextBlock()
        {
            Margin = new Thickness(0, 0, 4, 0),
            TextAlignment = System.Windows.TextAlignment.Center,
        };

        private TextBlock contentTextBlock = new TextBlock();

        private string filePath;

        public string FilePath
        {
            get => filePath.ToString();
            set
            {
                this.contentTextBlock.ToolTip =
                    filePath = value;

                var fi = new FileInfo(FilePath);
                this.Header = fi.Name;
                更新识别状态();
            }
        }

        private string header;
        /// <summary>
        /// 文件短名
        /// </summary>
        public string Header
        {
            get { return this.header; }
            set
            {
                this.header = value;

                try
                {
                    this.contentTextBlock.Text = this.索引 + ". " + new FileInfo(filePath).Name;
                }
                catch
                {
                    this.contentTextBlock.Text = this.索引 + ". " + "<未识别文件名>";
                }
            }
        }

        public Mat 鸟瞰图 { get; set; }

        public Mat 结果图 { get; set; }

        private 识别状态 _识别状态 = 识别状态.BE;

        public 识别状态 识别状态
        {
            get { return _识别状态; }
            set
            {
                _识别状态 = value;
                更新识别状态();
            }
        }

        public string 识别编号 { get; set; } = "";

        public string 错误信息 { get; set; } = "";

        public string 识别答案 { get; set; } = "";

        private bool _是否有空值 = false;

        public bool 是否有空值 { get { return _是否有空值; } }

        public int 答案数目
        {
            get
            {
                if (string.IsNullOrWhiteSpace(识别答案)) return 0;

                var txt = 识别答案.Replace(" ", "").Replace("　", "").Replace("\t", "");

                if (txt.Contains(";"))
                {
                    if (txt.EndsWith(";"))
                    {
                        txt = txt.Substring(0, txt.Length - 1);
                    }
                    var spans = txt.Split(new string[] { ";" }, StringSplitOptions.None);

                    var hasEmpty = false;
                    foreach (var span in spans)
                    {
                        if (string.IsNullOrWhiteSpace(span)) hasEmpty = true;
                    }

                    _是否有空值 = hasEmpty;
                    return spans.Length;
                }
                else
                {
                    _是否有空值 = false;
                    return 识别答案.Length;
                }
            }
        }
    }

    /// <summary>
    /// 为了多线程，必须脱离 UI，所以才有了这个类——它实际上几乎是 FileListBoxItem 类的翻版。
    /// </summary>
    public class AnswerSheetFile
    {
        public AnswerSheetFile()
        {
        }

        public AnswerSheetFile(string filePath)
        {
            FilePath = filePath;
        }

        public AnswerSheetFile(string filePath, Mat 鸟瞰图, Mat 结果图)
        {
            this.鸟瞰图 = 鸟瞰图;
            this.结果图 = 结果图;
            FilePath = filePath;
        }

        private string filePath;

        public string FilePath
        {
            get => filePath.ToString();
            set
            {
                filePath = value;
                var fi = new FileInfo(FilePath);
                this.Header = fi.Name;
            }
        }

        private string header;
        /// <summary>
        /// 文件短名
        /// </summary>
        public string Header
        {
            get { return this.header; }
            set { this.header = value; }
        }

        public Mat 鸟瞰图 { get; set; }

        public Mat 结果图 { get; set; }

        private 识别状态 _识别状态 = 识别状态.BE;

        public 识别状态 识别状态
        {
            get { return _识别状态; }
            set
            {
                _识别状态 = value;
            }
        }
        public string 错误信息 { get; set; } = "";

        public string 识别编号 { get; set; } = "";

        public string 识别答案 { get; set; } = "";

        private bool _是否有空值 = false;

        public bool 是否有空值 { get { return _是否有空值; } }

        public int 答案数目
        {
            get
            {
                if (string.IsNullOrWhiteSpace(识别答案)) return 0;

                var txt = 识别答案.Replace(" ", "").Replace("　", "").Replace("\t", "");

                if (txt.Contains(";"))
                {
                    if (txt.EndsWith(";"))
                    {
                        txt = txt.Substring(0, txt.Length - 1);
                    }
                    var spans = txt.Split(new string[] { ";" }, StringSplitOptions.None);

                    var hasEmpty = false;
                    foreach (var span in spans)
                    {
                        if (string.IsNullOrWhiteSpace(span)) hasEmpty = true;
                    }

                    _是否有空值 = hasEmpty;
                    return spans.Length;
                }
                else
                {
                    _是否有空值 = false;
                    return 识别答案.Length;
                }
            }
        }
    }

    public enum 识别状态
    {
        /// <summary>
        /// 表示尚未开始识别。
        /// </summary>
        BE,   // - 透明色

        /// <summary>
        /// 表示识别出错。
        /// </summary>
        ER,   // !    错误

        /// <summary>
        /// 表示识别结果存疑。
        /// </summary>
        WR,   // ?  存疑

        /// <summary>
        /// 表示识别成功（但并不保证完全无错——学生本身在涂卡时填涂的项仍然可能有问题）。
        /// </summary>
        FN,   // +   完成
    }

    public class AnswerSheetDefinitionFileItem : ListBoxItem
    {
        public AnswerSheetDefinitionFileItem(string filePath)
        {
            this.filePath = filePath;
            this.Content = this.Header;
        }

        private string filePath;

        public string FilePath { get => filePath; }

        public string Header
        {
            get
            {
                try
                {
                    var fi = new FileInfo(filePath);
                    if (File.Exists(fi.FullName))
                    {
                        return fi.Name;
                    }
                    else return "Not Exist";
                }
                catch (Exception ex)
                {
                    return $"Error: {ex.Message}";
                }
            }
        }
    }

    public class 识别结果
    {
        public string 识别编号 { get; set; }
        public string 识别答案文本 { get; set; }
    }
}
